package com.system.vote.admin.aspect;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.system.vote.common.params.Global;
import com.system.vote.common.utils.ByteUtils;
import com.system.vote.common.utils.DateUtil;
import com.system.vote.common.utils.StringUtils;
import com.system.vote.core.contant.HttpStatus;
import com.system.vote.core.http.Result;
import com.system.vote.admin.entity.SysAccount;
import com.system.vote.admin.entity.SysOperationLog;
import com.system.vote.admin.service.SysAccountService;
import com.system.vote.admin.service.SysOperationLogService;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.Executor;

/**
 * 系统日志切面
 *
 * @author CL
 */
@Log4j2
@Aspect
@Component
public class SysOperationLogAspect {

    private final ThreadLocal<SysOperationLog> sysLogThreadLocal = new ThreadLocal<>();

    @Autowired
    private Executor customThreadPoolTaskExecutor;

    @Autowired
    private SysOperationLogService sysLogService;

    @Autowired
    private SysAccountService sysAccountService;

    /**
     * 日志切点
     */
    @Pointcut("execution(public * com.system.vote.admin.controller.*.*(..))")
    public void sysOperationLogAspect() {
    }

    /**
     * 前置通知
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before(value = "sysOperationLogAspect()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

        SysOperationLog sysLog = new SysOperationLog();
        SysAccount sysAccount=null;
        // 创建人信息请根据实际项目获取方式获取
        if( SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof  String){
            sysLog.setCreateUserCode("");
            sysLog.setCreateUserName("");
        }
        else{
            sysAccount =(SysAccount) SecurityContextHolder.getContext().getAuthentication().getPrincipal() ;
            sysLog.setCreateUserName(sysAccount.getUsername());
            sysLog.setCreateUserCode(String.valueOf(sysAccount.getId()));
        }
        sysLog.setStartTime(new Date());
        sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
        sysLog.setRequestParams(formatParamsJoinPointArgs(joinPoint.getArgs()));
        sysLog.setRequestMethod(request.getMethod());
        sysLog.setRequestIp(ServletUtil.getClientIP(request));
        sysLog.setServerAddress(request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort());
        String userAgentStr = request.getHeader("User-Agent");
        sysLog.setUserAgent(userAgentStr);
        UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
        sysLog.setDeviceName(userAgent.getOs().getName());
        sysLog.setBrowserName(userAgent.getBrowser().getName());
        sysLog.setType(1);
        sysLogThreadLocal.set(sysLog);

        log.info("开始计时: {}  URI: {}  IP: {}", sysLog.getStartTime(), sysLog.getRequestUri(), sysLog.getRequestIp());
    }

    /**
     * 返回通知
     *
     * @param ret
     */
    @AfterReturning(pointcut = "sysOperationLogAspect()", returning = "ret")
    public void doAfterReturning(Object ret) {
        SysOperationLog sysLog = sysLogThreadLocal.get();
        sysLog.setLogType(Global.LOG_INGO);
        sysLog.setEndTime(new Date());
        sysLog.setExecuteTime((int) ChronoUnit.MINUTES.between(DateUtil.dateToTemporal(sysLog.getStartTime()), DateUtil.dateToTemporal(sysLog.getEndTime())));
        Result r = Convert.convert(Result.class, ret);
        if (r.get("code") == HttpStatus.SUCCESS + "") {
            sysLog.setIsException(Global.NO);
        } else {
            sysLog.setIsException(Global.YES);
            sysLog.setExceptionInfo((String) r.get("msg"));
        }
        customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
        sysLogThreadLocal.remove();

        Runtime runtime = Runtime.getRuntime();
        log.info("计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
                sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
                ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
    }

    /**
     * 异常通知
     *
     * @param
     */
    @AfterThrowing(pointcut = "sysOperationLogAspect()", throwing = "e")
    public void doAfterThrowable(Throwable e) {
        SysOperationLog sysLog = sysLogThreadLocal.get();
        sysLog.setLogType(Global.LOG_ERROR);
        sysLog.setEndTime(new Date());
        sysLog.setExecuteTime((int) ChronoUnit.MINUTES.between(DateUtil.dateToTemporal(sysLog.getStartTime()), DateUtil.dateToTemporal(sysLog.getEndTime())));
        sysLog.setIsException(Global.YES);
        sysLog.setExceptionInfo(e.getMessage());
        customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
        sysLogThreadLocal.remove();

        Runtime runtime = Runtime.getRuntime();
        log.info("计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
                sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
                ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
    }

    /**
     * 格式化参数
     *
     * @param parameterMap
     * @return
     */
    private String formatParams(Map<String, String[]> parameterMap) {
        if (parameterMap == null) {
            return null;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : (parameterMap).entrySet()) {
            if (params.length() != 0) {
                params.append("&");
            }
            params.append(param.getKey()).append("=");
            if (StrUtil.endWithIgnoreCase(param.getKey(), "password")) {
                params.append("*");
            } else if (param.getValue() != null) {
                params.append(ArrayUtil.join(param.getValue(), ","));
            }
        }
        log.info("params.toString{}", parameterMap.toString());
        return params.toString();
    }

    private String formatParamsJoinPointArgs(Object[] parameterMap) {
        if (parameterMap == null) {
            return "";
        }
        StringBuilder params = new StringBuilder();

        List<String> requestParams = new ArrayList<>();
        for (Object arg : parameterMap) {
            if (arg instanceof Map) {
                Map<String, Object> requestParamMap = (Map<String, Object>) arg;
                for (Map.Entry<String, Object> entry : requestParamMap.entrySet()) {
                    if (Objects.equals(entry.getKey(), "password")) {
                        requestParams.add(entry.getKey() + "=******");
                    } else {
                        requestParams.add(entry.getKey() + "=" + entry.getValue());
                    }
                }
                params.append(requestParams);
            } else if (arg instanceof String[]) {
                params.append(arg.toString());
            } else if (arg instanceof String) {
                params.append(((String) arg));
            } else {
                params.append(arg.toString());
            }
        }
        return params.toString();
    }

    /**
     * 保存日志线程
     */
    private static class SaveLogThread extends Thread {
        private final SysOperationLog sysLog;
        private final SysOperationLogService sysLogService;

        public SaveLogThread(SysOperationLog sysLog, SysOperationLogService sysLogService) {
            this.sysLog = sysLog;
            this.sysLogService = sysLogService;
        }

        @Override
        public void run() {
            sysLog.setCreateTime(new Date());
            sysLogService.save(sysLog);
        }
    }
}
